
namespace Patterns4Net.Core.CecilExtensions
{
    using System.Collections;
    using System.Collections.Generic;
    using System.Diagnostics.Contracts;
    using System.Linq;
    using Mono.Cecil;
    using Attributes.DesignPatterns;

    /// <summary>
    /// Represents the state of CIL stack at given point.
    /// </summary>
    [Immutable]
    public class StackState : IEnumerable<StackItem>
    {
        private readonly IEnumerable<StackItem> items;

        /// <summary>
        /// Initializes a new instance of the <see cref="StackState"/> class.
        /// </summary>
        /// <param name="items">Enumeration of items on stack in reverse order - first item is the stack's bottom.</param>
        public StackState(IEnumerable<StackItem> items)
        {
            Contract.Requires(items != null);
            this.items = items.ToArray();
        }

        public bool IsEmpty
        {
            get { return this.items.Count() == 0; }
        }

        public int Count
        {
            get { return this.items.Count(); }
        }

        /// <summary>
        /// Returns <see cref="StackItem"/> that represent parameters for 
        /// a method given method as if the method was called with this 
        /// stack state.
        /// </summary>
        /// <remarks>
        /// <para>It returns even 'this' parameter for instance methods, under the index 0.</para>
        /// </remarks>
        public StackItem[] GetArguments(MethodDefinition methodDefinition)
        {
            Contract.Requires(methodDefinition != null);
            Contract.Ensures(Contract.Result<StackItem[]>() != null);
            Contract.Ensures(Contract.Result<StackItem[]>().Count() >= methodDefinition.Parameters.Count);
            Contract.Assert(methodDefinition.Parameters != null);

            int takeCount = methodDefinition.Parameters.Count;
            if (methodDefinition.IsStatic == false)
            {
                takeCount++; // 'this' parameter
            }

            return this.items.Skip(this.items.Count() - takeCount).Take(takeCount).ToArray();
        }

        /// <summary>
        /// Creates the next stack state that is stack with given item on the top.
        /// </summary>
        public StackState Push(StackItem item)
        {
            Contract.Requires(item != null);
            Contract.Ensures(Contract.Result<StackState>() != null);
            var newItems = new List<StackItem>(this.items);
            newItems.Add(item);
            return new StackState(newItems);
        }

        /// <summary>
        /// Creates the next stack state that is stack without <paramref name="count"/> items from the top.
        /// </summary>
        public StackState Pop(int count = 1)
        {
            Contract.Requires(this.Count >= count);
            Contract.Ensures(Contract.Result<StackState>() != null);
            Contract.Ensures(Contract.Result<StackState>().Count == this.Count - count);
            return new StackState(this.items.Take(this.items.Count() - count));
        }

        public IEnumerator<StackItem> GetEnumerator()
        {
            return this.items.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.items.GetEnumerator();
        }

        [ContractInvariantMethod]
        private void ItemsNotNull()
        {
            Contract.Invariant(this.items != null);
        }
    }
}
